﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Data.Common;
using System.Data.Objects;
using System.Data;
using System.Data.EntityClient;
using System.Data.Entity;

namespace SystemHelperLibrary.EntityFramework
{
	public static class EntityFrameworkExt
	{
		/// <summary>
		/// Creates a store command based on the connection used by given object context.
		/// </summary>
		/// <param name="context">Object context.</param>
		/// <param name="commandText">Command text.</param>
		/// <param name="parameters">Parameters to pass to the store command.</param>
		/// <returns>Store command.</returns>
		public static DbCommand CreateStoreCommand(this ObjectContext context, string commandText, params object[] parameters)
		{
			return CreateStoreCommand(context, commandText, CommandType.Text, parameters);
		}

		/// <summary>
		/// Creates a store command based on the connection used by given object context.
		/// </summary>
		/// <param name="context">Object context.</param>
		/// <param name="commandText">Command text.</param>
		/// <param name="commandType">Command type.</param>
		/// <param name="parameters">Parameters to pass to the store command.</param>
		/// <returns>Store command.</returns>
		[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1062:Validate arguments of public methods", MessageId = "0"), System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Security", "CA2100:Review SQL queries for security vulnerabilities")]
		public static DbCommand CreateStoreCommand(this ObjectContext context, string commandText, CommandType commandType, params object[] parameters)
		{
			if (context == null)
				throw new ArgumentNullException("context", "Context cannot be null");

			EntityConnection entityConnection = (EntityConnection)context.Connection;
			DbConnection storeConnection = entityConnection.StoreConnection;
			DbCommand storeCommand = storeConnection.CreateCommand();

			// setup command
			storeCommand.CommandText = commandText;
			storeCommand.CommandType = commandType;
			if (null != parameters)
			{
				storeCommand.Parameters.AddRange(parameters);
			}

			// pass through command timeout as appropriate
			if (context.CommandTimeout.HasValue)
			{
				storeCommand.CommandTimeout = context.CommandTimeout.Value;
			}

			return storeCommand;
		}

		/// <summary>
		/// Associates results in the given source with an entity set. This supports tracking
		/// results in the state manager. If an existing element with the same key exists, it
		/// is returned instead.
		/// </summary>
		/// <typeparam name="TEntity">Entity type.</typeparam>
		/// <typeparam name="TBase">Base type for entity set.</typeparam>
		/// <param name="source">Entities to bind.</param>
		/// <param name="objectSet">Entity set to which elements should be bound.</param>
		/// <returns>Bound entities.</returns>
		public static IEnumerable<TEntity> Bind<TEntity, TBase>(this IEnumerable<TEntity> source, ObjectSet<TBase> objectSet)
			where TEntity : class, TBase
			where TBase : class
		{

			return source.Select(e => objectSet.FindOrAttach(e));
		}

		/// <summary>
		/// Attaches the given entity or returns existing entity with the same key.
		/// </summary>
		/// <typeparam name="TElement">Element type of object set.</typeparam>
		/// <typeparam name="TEntity">Entity type.</typeparam>
		/// <param name="objectSet">Object set.</param>
		/// <param name="entity">Entity to attach.</param>
		/// <returns>Input entity or existing entity with the same key.</returns>
		[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1062:Validate arguments of public methods", MessageId = "0")]
		public static TEntity FindOrAttach<TElement, TEntity>(this ObjectSet<TElement> objectSet, TEntity entity)
			where TEntity : class, TElement
			where TElement : class
		{
			if (objectSet == null)
				throw new ArgumentNullException("ObjectSet cannot be null");

			if (null == entity)
			{
				return null;
			}
			string qualifiedEntitySetName = objectSet.EntitySet.EntityContainer.Name + "." + objectSet.EntitySet.Name;
			EntityKey entityKey = objectSet.Context.CreateEntityKey(qualifiedEntitySetName, entity);
			ObjectStateEntry existingStateEntry;
			if (objectSet.Context.ObjectStateManager.TryGetObjectStateEntry(entityKey, out existingStateEntry) &&
				null != existingStateEntry.Entity) // A proxy entry may exist for the entity instance
			{
				try
				{
					return (TEntity)existingStateEntry.Entity;
				}
				catch (InvalidCastException)
				{
					throw new InvalidOperationException("Attached Entity Has WrongType");
				}
			}
			else
			{
				objectSet.Attach(entity);
				return entity;
			}
		}

		/// <summary>
		/// Gets all members of the ObjectSet that are currently in memory.
		/// </summary>
		/// <returns>All tracked members of the ObjectSet</returns>
		public static IEnumerable<TElement> GetTrackedEntities<TElement>(this ObjectSet<TElement> objectSet)
			where TElement : class
		{

			if (objectSet == null)
				throw new ArgumentNullException("ObjectSet cannot be null");

			return GetTrackedEntities<TElement>(objectSet, ~EntityState.Detached);
		}

		/// <summary>
		/// Gets all members of the ObjectSet that are currently in memory
		/// with the given state(s).
		/// </summary>
		/// <param name="objectSet">Object set.</param>
		/// <param name="state">Entity state flags.</param>
		/// <typeparam name="TElement">Element type of object set.</typeparam>
		/// <returns>Tracked members of the ObjectSet in the given state.</returns>
		public static IEnumerable<TElement> GetTrackedEntities<TElement>(this ObjectSet<TElement> objectSet, EntityState state)
			where TElement : class
		{
			if (objectSet == null)
				throw new ArgumentNullException("ObjectSet cannot be null");

			return objectSet.Context.ObjectStateManager.GetObjectStateEntries(state)
				.Where(entry => IsMemberOfObjectSet(objectSet, entry)).Select(e => e.Entity).Cast<TElement>();
		}

		private static bool IsMemberOfObjectSet<TElement>(ObjectSet<TElement> objectSet, ObjectStateEntry entry)
		   where TElement : class
		{
			return !entry.IsRelationship // must be an entity
				&& null != entry.Entity // must not be a key entry
				&& entry.EntitySet == objectSet.EntitySet; // must belong to the current set
		}

		/// <summary>
		/// Returns a handle on an IDisposable that can be used to safely control the lifetime
		/// of an open connection. If the connection is closed, it will be opened immediately
		/// and closed when the result of this method (the scope) is disposed. If the connection is already
		/// open, it remains open.
		/// <code>
		/// // Example with CreateConnectionScope
		/// using (command.Connection.CreateConnectionScope())
		/// {
		///     command.ExecuteNonQuery();
		/// }
		/// 
		/// // Example without
		/// bool connectionOpened = command.Connection.State == ConnectionState.Closed;
		/// if (connectionOpened)
		/// {
		///     command.Connection.Open();
		/// }
		/// try
		/// {
		///     command.ExecuteNonQuery();
		/// }
		/// finally
		/// {
		///     if (connectionOpened &amp;&amp; command.Connection.State == ConnectionState.Open)
		///     {
		///         command.Connection.Close();
		///     }
		/// }
		/// </code>
		/// </summary>
		/// <param name="connection">Connection to open.</param>
		/// <returns>Scope closing the connection on dispose.</returns>
		public static IDisposable CreateConnectionScope(this DbConnection connection)
		{
			if (connection == null)
				throw new ArgumentNullException("connection");

			return new OpenConnectionLifetime(connection);
		}
	}


	/// <summary>
	/// Class supporting the DataExtensions.OpenConnection method.
	/// </summary>
	internal class OpenConnectionLifetime : IDisposable
	{
		private readonly DbConnection connection;
		private readonly bool closeOnDispose;

		internal OpenConnectionLifetime(DbConnection connection)
		{
			this.connection = connection;
			this.closeOnDispose = connection.State == ConnectionState.Closed;
			if (this.closeOnDispose)
			{
				this.connection.Open();
			}
		}

		public void Dispose()
		{
			if (this.closeOnDispose && this.connection.State == ConnectionState.Open)
			{
				this.connection.Close();
			}
			GC.SuppressFinalize(this);
		}
	}


	public static class CacheService
	{
		private static Dictionary<string, object> queriesCache = new Dictionary<string, object>();
		private static readonly object locker = new object();
		/// <summary>
		/// Cache query to temporary storage
		/// </summary>
		/// <example>Orders.Where(p=>p.Price > 500).Cached("Orders");</example>
		/// <typeparam name="T"></typeparam>
		/// <param name="query"></param>
		/// <param name="cont"></param>
		/// <param name="key"></param>
		/// <returns></returns>
		public static IEnumerable<T> Cached<T>(this IQueryable<T> query, System.Data.Entity.DbContext cont, string key)
		where T : class
		{
			if (!queriesCache.ContainsKey(key))
			{
				lock (locker)
				{
					if (!queriesCache.ContainsKey(key))
					{
						cont.Configuration.ProxyCreationEnabled = false;
						var items = query.AsNoTracking().ToList();
						cont.Configuration.ProxyCreationEnabled = true;
						queriesCache[key] = items;
					}
				}
			}

			return (IEnumerable<T>)queriesCache[key];
		}
	}
}
